package org.itsolutions.javaBackup.buffers;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

import org.itsolutions.javaBackup.logger.ELogLevel;
import org.itsolutions.javaBackup.logger.ILogger;
import org.itsolutions.javaBackup.logger.LoggerHelper;
import org.itsolutions.javaBackup.spi.common.io.IByteBuffers;
import org.itsolutions.javaBackup.utils.IOUtils;

import com.google.inject.Inject;

/**
 * 
 * @author Tomas Fecko
 */
public final class ByteBuffers implements IByteBuffers {

	private static final ILogger			LOGGER = LoggerHelper.getLogger(ByteBuffers.class);
	private static final long				POOL_TIMEOUT_IN_SECONDS = 5;
	private final ByteBufferPool 			mPool;
	private final List<ByteBufferWrapper>	mBuffers = new ArrayList<ByteBufferWrapper>();
	private ByteBuffer 						mCurrentByteBuffer = null;
	private volatile int					mCurrentByteBufferIndex;
	private volatile int 					mRemaining;
	private volatile int					mPosition;
//	private final String 					mUID = UUID.randomUUID().toString();

	/**
	 * 
	 * @param pPool byte buffer pool
	 * TODO fix public -> should be friendly
	 */
	@Inject public ByteBuffers(ByteBufferPool pPool) {
		mPool = pPool;
	}

	@Override
	public int getNumberOfBuffers() {
		return mBuffers.size();
	}
	
	@Override
	public int getAvailablePooledBuffers() {
		return mPool.getAvailablePooledBuffers();
	}
	
	@Override
	public int getUsedBuffersCount() {
		return mPool.getUsedBuffersCount();
	}

	@Override
	public int releaseByteBuffers() {
		LOGGER.log(ELogLevel.TRACE, "Releasing ", mBuffers.size(), " byteBuffers back to pool. Available byteBuffers in pool:", 
				getAvailablePooledBuffers(), " Used byteBuffers from pool:", getUsedBuffersCount());
		int count = 0;
		mCurrentByteBuffer = null;
		mCurrentByteBufferIndex = 0;
		mRemaining = 0;
		mPosition = 0;
		if (mBuffers != null && !mBuffers.isEmpty()) {
			for (int x = 0; x < mBuffers.size(); x++) {
				if (mBuffers.get(x).releaseByteBuffer()) {
					count++;
				}
			}
			mBuffers.clear();
			count++;
		}
		return count;
	}

	@Override
	public ByteBuffers bufferFile(File pFile) {
//		LOGGER.log(DEBUG, mUID, " Buffering file:", pFile);
		if (!mBuffers.isEmpty()) {
			for (ByteBufferWrapper byteBuffer : mBuffers) {
				byteBuffer.releaseByteBuffer();
			}
			mBuffers.clear();
		}
		FileInputStream fis = null;
		try {
			fis = new FileInputStream(pFile);
			FileChannel fc = fis.getChannel();
			ByteBuffer buffer = getByteBuffer();
			while (fc.read(buffer) != -1) {
				if (!buffer.hasRemaining()) {
					buffer.flip();
					buffer = getByteBuffer();
				}
			}
			if (buffer.position() > 0) {
				buffer.flip();
			}
		} catch (IOException e) {
			LOGGER.log(ELogLevel.ERROR, e);
		} finally {
			if (fis != null) {
				try {
					fis.close();
				} catch (IOException e) {
					LOGGER.log(ELogLevel.ERROR, e);
				}
			}
		}
		init();
		return this;
	}

	@Override
	public ByteBuffers bufferArray(byte[] pArray) {
		if (!mBuffers.isEmpty()) {
			for (ByteBufferWrapper byteBuffer : mBuffers) {
				byteBuffer.releaseByteBuffer();
			}
			mBuffers.clear();
		}
		try {
			if (pArray == null || pArray.length == 0) {
				return this;
			}
			ByteBuffer buffer = getByteBuffer();
			for (int x = 0; x < pArray.length; x++) {
				buffer.put(pArray[x]);
				if (!buffer.hasRemaining()) {
					buffer.flip();
					buffer = getByteBuffer();
				}
			}
			if (buffer.position() > 0) {
				buffer.flip();
			}
			return this;
		} finally {
			init();
		}
	}

	@Override
	public byte[] getArray() {
		if (mBuffers == null || mBuffers.isEmpty()) {
			return new byte[0];
		}
		int size = 0;
		for (int x = 0; x < mBuffers.size(); x++) {
			size += mBuffers.get(x).limit();
		}
		byte[] returnArray = new byte[size];
		int offset = 0;
		ByteBuffer bb;
		for (int x = 0; x < mBuffers.size(); x++) {
			bb = mBuffers.get(x).getByteBuffer();
			bb.get(returnArray, offset, bb.limit());
			bb.flip();
			offset += bb.limit();
		}
		init();
		return returnArray;
	}


	@Override
	public byte[] getArray(int pSizeOfArray) {
		if (mBuffers == null || mBuffers.isEmpty()) {
			return new byte[0];
		}
		if (remaining() < pSizeOfArray) {
			return null;
		}
		byte[] returnArray = new byte[pSizeOfArray];
		for (int x = 0; x < pSizeOfArray; x++) {
			returnArray[x] = getByte();
		}
		return returnArray;
	}

	@Override
	public int position() {
		return mPosition;
	}
	
	@Override
	public int remaining() {
		return mRemaining;
	}
	
	@Override
	public boolean getBoolean() {
		if (mCurrentByteBuffer.remaining() >= 1) {
			mRemaining--;
			mPosition++;
			return IOUtils.makeBoolean(mCurrentByteBuffer.get());
		} else if (mRemaining < 1) {
			throw new BufferUnderflowException();
		} else {
			mCurrentByteBuffer = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			mRemaining--;
			mPosition++;
			return IOUtils.makeBoolean(mCurrentByteBuffer.get());
		}
	}
	
	@Override
	public byte getByte() {
		if (mCurrentByteBuffer.remaining() >= 1) {
			mRemaining--;
			mPosition++;
			return mCurrentByteBuffer.get();
		} else if (mRemaining < 1) {
			throw new BufferUnderflowException();
		} else {
			mCurrentByteBuffer = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			mRemaining--;
			mPosition++;
			return mCurrentByteBuffer.get();
		}
	}
	
	@Override
	public int getShort() {
		if (mCurrentByteBuffer.remaining() >= 2) {
			mRemaining -= 2;
			mPosition += 2;
			return mCurrentByteBuffer.getInt();
		} else if (mRemaining < 2) {
			throw new BufferUnderflowException();
		} else {
			ByteBuffer next = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			short returnNumber = Short.MIN_VALUE;
			if (mCurrentByteBuffer.remaining() == 1) {
				returnNumber = IOUtils.makeShort(mCurrentByteBuffer.get(), next.get());
			} else {
				returnNumber = next.getShort();
			}
			mCurrentByteBuffer = next;
			mRemaining -= 2;
			mPosition += 2;
			return returnNumber;
		}
	}
	
	@Override
	public int getInt() {
		if (mCurrentByteBuffer.remaining() >= 4) {
			mRemaining -= 4;
			mPosition += 4;
			return mCurrentByteBuffer.getInt();
		} else if (mRemaining < 4) {
			throw new BufferUnderflowException();
		} else {
			ByteBuffer next = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			int returnNumber = Integer.MIN_VALUE;
			switch (mCurrentByteBuffer.remaining()) {
			case 3:
				returnNumber = IOUtils.makeInt(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get());
				break;
			case 2:
				returnNumber = IOUtils.makeInt(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), next.get());
				break;
			case 1:
				returnNumber = IOUtils.makeInt(mCurrentByteBuffer.get(), next.get(), next.get(), next.get());
				break;
			default:
				returnNumber = next.getInt();
			}
			mCurrentByteBuffer = next;
			mRemaining -= 4;
			mPosition += 4;
			return returnNumber;
		}
	}

	@Override
	public float getFloat() {
		if (mCurrentByteBuffer.remaining() >= 4) {
			mRemaining -= 4;
			mPosition += 4;
			return mCurrentByteBuffer.getFloat();
		} else if (mRemaining < 4) {
			throw new BufferUnderflowException();
		} else {
			ByteBuffer next = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			float returnNumber = Float.MIN_VALUE;
			switch (mCurrentByteBuffer.remaining()) {
			case 3:
				returnNumber = IOUtils.bytesToFloat(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get());
				break;
			case 2:
				returnNumber = IOUtils.bytesToFloat(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), next.get());
				break;
			case 1:
				returnNumber = IOUtils.bytesToFloat(mCurrentByteBuffer.get(), next.get(), next.get(), next.get());
				break;
			default:
				returnNumber = next.getFloat();
			}
			mCurrentByteBuffer = next;
			mRemaining -= 4;
			mPosition += 4;
			return returnNumber;
		}
	}

	@Override
	public long getLong() {
		if (mCurrentByteBuffer.remaining() >= 8) {
			mRemaining -= 8;
			mPosition += 8;
			return mCurrentByteBuffer.getLong();
		} else if (mRemaining < 8) {
			throw new BufferUnderflowException();
		} else {
			ByteBuffer next = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			long returnNumber = Long.MIN_VALUE;
			switch (mCurrentByteBuffer.remaining()) {
			case 7:
				returnNumber = IOUtils.makeLong(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get());
				break;
			case 6:
				returnNumber = IOUtils.makeLong(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), next.get());
				break;
			case 5:
				returnNumber = IOUtils.makeLong(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), next.get(), next.get());
				break;
			case 4:
				returnNumber = IOUtils.makeLong(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), next.get(), next.get(), next.get(), next.get());
				break;
			case 3:
				returnNumber = IOUtils.makeLong(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						next.get(), next.get(), next.get(), next.get(), next.get());
				break;
			case 2:
				returnNumber = IOUtils.makeLong(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), 
						next.get(), next.get(), next.get(), next.get(), next.get());
				break;
			case 1:
				returnNumber = IOUtils.makeLong(mCurrentByteBuffer.get(), next.get(), next.get(), 
						next.get(), next.get(), next.get(), next.get(), next.get());
				break;
			default:
				returnNumber = next.getLong();
			}
			mCurrentByteBuffer = next;
			mRemaining -= 8;
			mPosition += 8;
			return returnNumber;
		}
	}

	@Override
	public double getDouble() {
		if (mCurrentByteBuffer.remaining() >= 8) {
			mRemaining -= 8;
			mPosition += 8;
			return mCurrentByteBuffer.getDouble();
		} else if (mRemaining < 8) {
			throw new BufferUnderflowException();
		} else {
			ByteBuffer next = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			double returnNumber = Double.MIN_VALUE;
			switch (mCurrentByteBuffer.remaining()) {
			case 7:
				returnNumber = IOUtils.bytesToDouble(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get());
				break;
			case 6:
				returnNumber = IOUtils.bytesToDouble(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), next.get());
				break;
			case 5:
				returnNumber = IOUtils.bytesToDouble(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), next.get(), next.get());
				break;
			case 4:
				returnNumber = IOUtils.bytesToDouble(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						mCurrentByteBuffer.get(), next.get(), next.get(), next.get(), next.get());
				break;
			case 3:
				returnNumber = IOUtils.bytesToDouble(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), 
						next.get(), next.get(), next.get(), next.get(), next.get());
				break;
			case 2:
				returnNumber = IOUtils.bytesToDouble(mCurrentByteBuffer.get(), mCurrentByteBuffer.get(), next.get(), 
						next.get(), next.get(), next.get(), next.get(), next.get());
				break;
			case 1:
				returnNumber = IOUtils.bytesToDouble(mCurrentByteBuffer.get(), next.get(), next.get(), 
						next.get(), next.get(), next.get(), next.get(), next.get());
				break;
			default:
				returnNumber = next.getDouble();
			}
			mCurrentByteBuffer = next;
			mRemaining -= 8;
			mPosition += 8;
			return returnNumber;
		}
	}

	@Override
	public IByteBuffers moveForward(int pNumberOfBytes) {
		if (pNumberOfBytes > mRemaining) {
			throw new BufferUnderflowException();
		}
		while (pNumberOfBytes > 0) {
			int remaining = mCurrentByteBuffer.remaining();
			if (pNumberOfBytes > remaining) {
				mCurrentByteBuffer.position(mCurrentByteBuffer.position() + remaining);
				mRemaining -= remaining;
				mPosition += remaining;
				pNumberOfBytes -= remaining;
				mCurrentByteBuffer = mBuffers.get(++mCurrentByteBufferIndex).getByteBuffer();
			} else {
				mCurrentByteBuffer.position(mCurrentByteBuffer.position() + pNumberOfBytes);
				mRemaining -= pNumberOfBytes;
				mPosition += pNumberOfBytes;
				pNumberOfBytes = 0;
			}
		}
		return this;
	}

	/**
	 * 
	 * @return
	 */
	private ByteBuffer getByteBuffer() {
		ByteBufferWrapper buffer = mPool.pollFreeBuffer(POOL_TIMEOUT_IN_SECONDS, SECONDS);
		if (buffer == null) {
			LOGGER.log(ELogLevel.WARN, "There are no free byteBuffers in pool for ", POOL_TIMEOUT_IN_SECONDS, 
					" seconds. Allocating byteBuffer out of the pool. This could lead to Out of memory problems, if heap space is depleted!!!");
			buffer = new ByteBufferWrapper(ByteBuffer.allocate(mPool.getSizeOfBuffer()));
		}
		mBuffers.add(buffer);
		return buffer.getByteBuffer();
	}
	
	/**
	 * 
	 */
	private void init() {
		mRemaining = 0;
		mPosition = 0;
		mCurrentByteBufferIndex = 0;			
		if (mBuffers.isEmpty()) {
			mCurrentByteBuffer = null;
		} else {
			mCurrentByteBuffer = mBuffers.get(mCurrentByteBufferIndex).getByteBuffer();
			for (int x = 0; x < mBuffers.size(); x++) {
				mRemaining += mBuffers.get(x).getByteBuffer().remaining();
			}
		}
//		LOGGER.log(DEBUG, mUID, " REMAINING:", mRemaining);
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		if (mBuffers != null && !mBuffers.isEmpty()) {
			int count = releaseByteBuffers();
			LOGGER.log(ELogLevel.TRACE, "Automaticaly released ", count, " byte buffers.");
		}
	}
}







